Praca domowa #3 -Marika Partyka

Do pracy domowej używam zbioru z pierwszej pracy domowej, czyli mushrooms.

In [9]:
import sklearn
import sklearn.ensemble
import pandas as pd
import numpy as np
import lime
import lime.lime_tabular
from sklearn.linear_model import LogisticRegression
import random
random.seed(123)

Zmieniam nazwy klas na "0" i "1".

In [10]:
data = pd.read_csv("/home/user/Pobrane/mushroom-classification/mushrooms.csv")
feature_names = list(data.columns[1:])
data = data.to_numpy()
labels = data[:,0]
le= sklearn.preprocessing.LabelEncoder()
le.fit(labels)
labels = le.transform(labels)
class_names = le.classes_
categorical_features = range(22)
data = data[:,1:]

Zmienne są kategoryczne i zapisane za pomocą skrótów, dlatego rozwijam je do pełnych nazw, a następnie koduje za pomocą OneHotEncoder.

In [11]:
categorical_names = '''bell=b,conical=c,convex=x,flat=f,knobbed=k,sunken=s
fibrous=f,grooves=g,scaly=y,smooth=s
brown=n,buff=b,cinnamon=c,gray=g,green=r,pink=p,purple=u,red=e,white=w,yellow=y
bruises=t,no=f
almond=a,anise=l,creosote=c,fishy=y,foul=f,musty=m,none=n,pungent=p,spicy=s
attached=a,descending=d,free=f,notched=n
close=c,crowded=w,distant=d
broad=b,narrow=n
black=k,brown=n,buff=b,chocolate=h,gray=g,green=r,orange=o,pink=p,purple=u,red=e,white=w,yellow=y
enlarging=e,tapering=t
bulbous=b,club=c,cup=u,equal=e,rhizomorphs=z,rooted=r,missing=?
fibrous=f,scaly=y,silky=k,smooth=s
fibrous=f,scaly=y,silky=k,smooth=s
brown=n,buff=b,cinnamon=c,gray=g,orange=o,pink=p,red=e,white=w,yellow=y
brown=n,buff=b,cinnamon=c,gray=g,orange=o,pink=p,red=e,white=w,yellow=y
partial=p,universal=u
brown=n,orange=o,white=w,yellow=y
none=n,one=o,two=t
cobwebby=c,evanescent=e,flaring=f,large=l,none=n,pendant=p,sheathing=s,zone=z
black=k,brown=n,buff=b,chocolate=h,green=r,orange=o,purple=u,white=w,yellow=y
abundant=a,clustered=c,numerous=n,scattered=s,several=v,solitary=y
grasses=g,leaves=l,meadows=m,paths=p,urban=u,waste=w,woods=d'''.split('\n')

for j, names in enumerate(categorical_names):

    values = names.split(',')
    values = dict([(x.split('=')[1], x.split('=')[0]) for x in values])
    print(values)
    data[:,j] = np.array(list(map(lambda x: values[x], data[:,j])))
    # print(data[:,j])


categorical_names = {}
for feature in categorical_features:
    le = sklearn.preprocessing.LabelEncoder()
    le.fit(data[:, feature])
    data[:, feature] = le.transform(data[:, feature])
    categorical_names[feature] = le.classes_
{'b': 'bell', 'c': 'conical', 'x': 'convex', 'f': 'flat', 'k': 'knobbed', 's': 'sunken'}
{'f': 'fibrous', 'g': 'grooves', 'y': 'scaly', 's': 'smooth'}
{'n': 'brown', 'b': 'buff', 'c': 'cinnamon', 'g': 'gray', 'r': 'green', 'p': 'pink', 'u': 'purple', 'e': 'red', 'w': 'white', 'y': 'yellow'}
{'t': 'bruises', 'f': 'no'}
{'a': 'almond', 'l': 'anise', 'c': 'creosote', 'y': 'fishy', 'f': 'foul', 'm': 'musty', 'n': 'none', 'p': 'pungent', 's': 'spicy'}
{'a': 'attached', 'd': 'descending', 'f': 'free', 'n': 'notched'}
{'c': 'close', 'w': 'crowded', 'd': 'distant'}
{'b': 'broad', 'n': 'narrow'}
{'k': 'black', 'n': 'brown', 'b': 'buff', 'h': 'chocolate', 'g': 'gray', 'r': 'green', 'o': 'orange', 'p': 'pink', 'u': 'purple', 'e': 'red', 'w': 'white', 'y': 'yellow'}
{'e': 'enlarging', 't': 'tapering'}
{'b': 'bulbous', 'c': 'club', 'u': 'cup', 'e': 'equal', 'z': 'rhizomorphs', 'r': 'rooted', '?': 'missing'}
{'f': 'fibrous', 'y': 'scaly', 'k': 'silky', 's': 'smooth'}
{'f': 'fibrous', 'y': 'scaly', 'k': 'silky', 's': 'smooth'}
{'n': 'brown', 'b': 'buff', 'c': 'cinnamon', 'g': 'gray', 'o': 'orange', 'p': 'pink', 'e': 'red', 'w': 'white', 'y': 'yellow'}
{'n': 'brown', 'b': 'buff', 'c': 'cinnamon', 'g': 'gray', 'o': 'orange', 'p': 'pink', 'e': 'red', 'w': 'white', 'y': 'yellow'}
{'p': 'partial', 'u': 'universal'}
{'n': 'brown', 'o': 'orange', 'w': 'white', 'y': 'yellow'}
{'n': 'none', 'o': 'one', 't': 'two'}
{'c': 'cobwebby', 'e': 'evanescent', 'f': 'flaring', 'l': 'large', 'n': 'none', 'p': 'pendant', 's': 'sheathing', 'z': 'zone'}
{'k': 'black', 'n': 'brown', 'b': 'buff', 'h': 'chocolate', 'r': 'green', 'o': 'orange', 'u': 'purple', 'w': 'white', 'y': 'yellow'}
{'a': 'abundant', 'c': 'clustered', 'n': 'numerous', 's': 'scattered', 'v': 'several', 'y': 'solitary'}
{'g': 'grasses', 'l': 'leaves', 'm': 'meadows', 'p': 'paths', 'u': 'urban', 'w': 'waste', 'd': 'woods'}

Rozdzielam dane na zbiór uczący i testowy.

In [12]:
data = data.astype(float)
train, test, labels_train, labels_test = sklearn.model_selection.train_test_split(data, labels, train_size=0.70)
encoder = sklearn.preprocessing.OneHotEncoder()
encoder.fit(data)
encoded_train = encoder.transform(train)
    

MODEL I - RandomForest

In [13]:
rf = sklearn.ensemble.RandomForestClassifier(n_estimators=100)
rf.fit(encoded_train, labels_train)
sklearn.metrics.accuracy_score(labels_test, rf.predict(encoder.transform(test)))
Out[13]:
1.0

Wyjaśnianie metodą LIME

In [14]:
explainer = lime.lime_tabular.LimeTabularExplainer(train ,class_names=['jadalny', 'trujący'], feature_names = feature_names,
                                                   categorical_features=categorical_features, 
                                                   categorical_names=categorical_names)
predict_fn_rf = lambda x: rf.predict_proba(encoder.transform(x))

Weźmy 25 obserwację ze zbioru testowego i zobaczmy jak wygląda wyjaśnienie. Klasyfikator RF zaklasyfikował tę obserwację jako grzyb jadalny, największy wpływ na to miała zmienna "odor" mająca wartość "none". Zmienna, która mogłaby przemawiać za tym, że jest to grzyb trujący to "bruises".

In [22]:
exp1 = explainer.explain_instance(test[25], predict_fn_rf)
exp1.show_in_notebook()

Dla zmiennej o numerze 12 w zbiorze testowym mamy następujące wyjaśnienie: zaklasyfikowano do klasy "trujący", tutaj także największe znaczenie miała zmienna "odor".

In [23]:
exp2 = explainer.explain_instance(test[12], predict_fn_rf, num_features=22)
exp2.show_in_notebook()

Akurat te dwie zmienne miały prawdopodobiestwo zaklasyfikowania do klas równe 1. Oznacza to, że poza zmiennymi, które są bardzo istotne w predykcji tego problemu, czyli w tym przypadku : odor i gill-size, małe zmianny w wartościach innych parametrów nie zmieniłoby decyzji klasyfikatora. Być może przy zmianie wartości zmiennej "odor" dla pierwszego przykładu, prawdopodobieństwo zaklasyfikowania mogłoby się zmienić, sprawdźmy to.

In [24]:
test[25,4]=4.0 # zmiana wartości "odor" na "foul", bez zmiany innych parametrów.
# test[25]
exp2new = explainer.explain_instance(test[25], predict_fn_rf, num_features=22)
exp2new.show_in_notebook()

Jak widać, zmiana ważnego parametru zmieniła znacznie prawdopodobieństwo przypisania do klas.

MODEL II - LogisticRegression

In [25]:
test[25,4]=6.0 
l_reg=LogisticRegression()
l_reg.fit(encoded_train, labels_train)
sklearn.metrics.accuracy_score(labels_test, l_reg.predict(encoder.transform(test)))
                                                   

predict_regl = lambda x: l_reg.predict_proba(encoder.transform(x))

exp = explainer.explain_instance(test[12], predict_regl, num_features=22)
exp.show_in_notebook()

Model regresji logistycznej także zaklasyfikował 12 obserwacje jako grzyb trujący z prawdopodobieństwem 1, jednak pojawiają się małe różnice w wyjaśnieniu. Przede wszystkim zmienna "odor" dla regresji logistycznej jest bardziej znacząca a zmienna "bruises" w tym modelu przemawiała (choć jest to bardzo mały wkład) za tym, że jest to grzyb trujący, w przeciwieństwie do modelu pierwszego, gdzie wartość tej zmiennej była po stronie klasyfikującej obserwację jako jadalną.